今天我們來了解如何利用pl.Expr.str進行pl.String
的各種操作。
本日大綱如下:
pl.Expr.str
提供的exprcodepanda
import pandas as pd
import polars as pl
import pyarrow as pa
data = {
"colors": ["black", "white", "yellow"],
"fruits": ["APPLE", "BANANA", "STRAWBERRY"],
"animals": ["dog", "cat", "squirrel"],
}
df = pl.DataFrame(data)
shape: (3, 3)
┌────────┬────────────┬──────────┐
│ colors ┆ fruits ┆ animals │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞════════╪════════════╪══════════╡
│ black ┆ APPLE ┆ dog │
│ white ┆ BANANA ┆ cat │
│ yellow ┆ STRAWBERRY ┆ squirrel │
└────────┴────────────┴──────────┘
pl.Expr.str
提供的exprpl.Expr.str
提供了許多好用的expr,讓使用者就像在操作Python的str
型態一樣,但卻能擁有向量化的效率,而不必訴諸於迴圈,以下將舉幾個例子說明。
此外,str
命名空間中許多expr都會接受正則表達式為輸入,例如pl.Expr.str.contains(),一般來說這是多數使用者想要的功能。如果您不需要這個功能的話,需要將literal=
設為True
,這樣一來Polars就不會對輸入先進行正則表達式的解析。
可以使用pl.Expr.str.starts_with()或pl.Expr.str.ends_with()來確認字串的開頭或結尾。例如我們想確認「"animals"」列是否有「"t"」結尾的字串,可以這麼寫:
df.select(
pl.col("animals"),
pl.col("animals").str.ends_with("t").name.suffix("_t$"),
)
shape: (3, 2)
┌──────────┬────────────┐
│ animals ┆ animals_t$ │
│ --- ┆ --- │
│ str ┆ bool │
╞══════════╪════════════╡
│ dog ┆ false │
│ cat ┆ true │
│ squirrel ┆ false │
└──────────┴────────────┘
我們可以使用pl.Expr.str.contains()來確認各行的字串是否含有某些字串。例如,確認所有列是否含有「"rr"」字串:
df.with_columns(pl.all().str.contains("rr").name.suffix("_c"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_c ┆ fruits_c ┆ animals_c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ bool ┆ bool ┆ bool │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ false ┆ false ┆ false │
│ white ┆ BANANA ┆ cat ┆ false ┆ false ┆ false │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false ┆ false ┆ true │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘
pl.Expr.str.contains()
接受正則表達式,所以如果想確認所有列是否含有「"pp"」字串(無論大小寫):
df.with_columns(pl.all().str.contains("(?i)pp").name.suffix("_c"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_c ┆ fruits_c ┆ animals_c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ bool ┆ bool ┆ bool │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ false ┆ true ┆ false │
│ white ┆ BANANA ┆ cat ┆ false ┆ false ┆ false │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false ┆ false ┆ false │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘
此外,如果是有多個字串想要確認的話,可以使用pl.Expr.str.contains_any()。例如想確認所有列是否含有「"PP"」或是「"RR"」字串:
(
df.with_columns(
pl.all().str.contains_any(["PP", "RR"]).name.suffix("_c")
)
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_c ┆ fruits_c ┆ animals_c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ bool ┆ bool ┆ bool │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ false ┆ true ┆ false │
│ white ┆ BANANA ┆ cat ┆ false ┆ false ┆ false │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false ┆ true ┆ false │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘
如果是想不區別大小寫的話,可以將pl.Expr.str.contains_any()
中的ascii_case_insensitive=
設為True
:
(
df.with_columns(
pl.all()
.str.contains_any(["PP", "RR"], ascii_case_insensitive=True)
.name.suffix("_c")
)
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_c ┆ fruits_c ┆ animals_c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ bool ┆ bool ┆ bool │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ false ┆ true ┆ false │
│ white ┆ BANANA ┆ cat ┆ false ┆ false ┆ false │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false ┆ true ┆ true │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘
我們可以使用以下兩種expr來確認字串是否符合正則表達式:
group_index=
來指定想要的分組。舉例來說:
pl.col("fruits").str.extract("BA(NA)", group_index=0).alias("extract")
查看「"fruits"」列中是否有「"BA(NA)"」這樣型式的字串。此處使用group_index=0
抓取全部符合樣式的字串(group_index
預設為1,會抓取到「"NA"」)。pl.col("fruits").str.extract_all("(NA)").alias("extract_all")
查看「"fruits"」列中是否有「"(NA)"」這樣型式的字串。可以看出其成功找出了兩個符合的結果,即兩個「"NA"」。df.select(
pl.col("fruits"),
pl.col("fruits").str.extract("BA(NA)", group_index=0).alias("extract"),
pl.col("fruits").str.extract_all("(NA)").alias("extract_all"),
)
shape: (3, 3)
┌────────────┬─────────┬──────────────┐
│ fruits ┆ extract ┆ extract_all │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ list[str] │
╞════════════╪═════════╪══════════════╡
│ APPLE ┆ null ┆ [] │
│ BANANA ┆ BANA ┆ ["NA", "NA"] │
│ STRAWBERRY ┆ null ┆ [] │
└────────────┴─────────┴──────────────┘
我們可以使用以下三種expr來取代部份字串:
以下舉例說明:
pl.col("fruits").str.replace("A", "a").alias("r")
將「"fruits"」列中的第一個「"A"」取代為「"a"」。 pl.col("fruits").str.replace_all("A", "a").alias("r_all")
將「"fruits"」列中所有「"A"」皆取代為「"a"」。pl.col("fruits").str.replace_many(["A", "P"], ["a", "p"]).alias("r_many_list")
將「"fruits"」列中所有「"A"」皆取代為「"a"」及將所有「"P"」皆取代為「"p"」。這裡使用兩個列表來表達取代的對應關係。pl.col("fruits").str.replace_many({"A": "a", "P": "p"}).alias("r_many_dict")
將「"fruits"」列中所有「"A"」皆取代為「"a"」及將所有「"P"」皆取代為「"p"」。這裡使用一個字典表達取代的對應關係。df.select(
pl.col("fruits"),
pl.col("fruits").str.replace("A", "a").alias("r"),
pl.col("fruits").str.replace_all("A", "a").alias("r_all"),
pl.col("fruits")
.str.replace_many(["A", "P"], ["a", "p"])
.alias("r_many_list"),
pl.col("fruits")
.str.replace_many({"A": "a", "P": "p"})
.alias("r_many_dict"),
)
shape: (3, 5)
┌────────────┬────────────┬────────────┬─────────────┬─────────────┐
│ fruits ┆ r ┆ r_all ┆ r_many_list ┆ r_many_dict │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ str ┆ str │
╞════════════╪════════════╪════════════╪═════════════╪═════════════╡
│ APPLE ┆ aPPLE ┆ aPPLE ┆ appLE ┆ appLE │
│ BANANA ┆ BaNANA ┆ BaNaNa ┆ BaNaNa ┆ BaNaNa │
│ STRAWBERRY ┆ STRaWBERRY ┆ STRaWBERRY ┆ STRaWBERRY ┆ STRaWBERRY │
└────────────┴────────────┴────────────┴─────────────┴─────────────┘
大小寫等轉換十分簡單,可以使用pl.Expr.str.to_uppercase()、pl.Expr.str.to_lowercase()及pl.Expr.str.to_titlecase()來達成。
(
df.select(
pl.col("colors").str.to_uppercase(),
pl.col("fruits").str.to_lowercase(),
pl.col("animals").str.to_titlecase(),
)
)
shape: (3, 3)
┌────────┬────────────┬──────────┐
│ colors ┆ fruits ┆ animals │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞════════╪════════════╪══════════╡
│ BLACK ┆ apple ┆ Dog │
│ WHITE ┆ banana ┆ Cat │
│ YELLOW ┆ strawberry ┆ Squirrel │
└────────┴────────────┴──────────┘
計算字串長度主要依靠pl.Expr.str.len_bytes()及pl.Expr.str.len_chars()兩個expr。
當處理ASCII字串時,每個character僅會使用一個byte,所以兩者會得到一樣的答案。由於pl.Expr.str.len_bytes()
的計算複雜度為O(1)
,而pl.Expr.str.len_chars()
的計算複雜度為O(n)
,故推薦使用pl.Expr.str.len_bytes()
。
但當處理非ASCII字串時,每個character最多可以使用四個byte,多出來的byte會用來存儲輔音或字型等資料。舉例來說,下面例子幫助我們了解「"2025IThome鐵人賽"」這個字串分別是多少byte及多少character:
with pl.Config(tbl_rows=20):
pl.DataFrame({"col": list("2025IThome鐵人賽")}).select(
pl.col("col"),
pl.col("col").str.len_bytes().alias("n_bytes"),
pl.col("col").str.len_chars().alias("n_chars"),
)
shape: (13, 3)
┌─────┬─────────┬─────────┐
│ col ┆ n_bytes ┆ n_chars │
│ --- ┆ --- ┆ --- │
│ str ┆ u32 ┆ u32 │
╞═════╪═════════╪═════════╡
│ 2 ┆ 1 ┆ 1 │
│ 0 ┆ 1 ┆ 1 │
│ 2 ┆ 1 ┆ 1 │
│ 5 ┆ 1 ┆ 1 │
│ I ┆ 1 ┆ 1 │
│ T ┆ 1 ┆ 1 │
│ h ┆ 1 ┆ 1 │
│ o ┆ 1 ┆ 1 │
│ m ┆ 1 ┆ 1 │
│ e ┆ 1 ┆ 1 │
│ 鐵 ┆ 3 ┆ 1 │
│ 人 ┆ 3 ┆ 1 │
│ 賽 ┆ 3 ┆ 1 │
└─────┴─────────┴─────────┘
由於數字及英文字母都是ASCII字串,所以其使用的byte及character數目都為一。而「"鐵人賽"」這三個正體中文字由於不是ASCII字串,所以每個字需要使用三個byte,而character數目仍為一。
我們可以使用pl.Expr.str.slice()來截取部份字串,其接受offset=
及length=
兩個參數。offset=
為起始索引值,可以接受負整數,代表由字串後面開始索引。而length=
則為截取字串長度,預設為None
,代表截取至字串尾端。舉例來說,如果我們只想截取每一列除了第一個字元之外的字串,可以這麼寫:
df.with_columns(pl.all().str.slice(1).name.suffix("_s"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬───────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_s ┆ fruits_s ┆ animals_s │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ str ┆ str ┆ str │
╞════════╪════════════╪══════════╪══════════╪═══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ lack ┆ PPLE ┆ og │
│ white ┆ BANANA ┆ cat ┆ hite ┆ ANANA ┆ at │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ ellow ┆ TRAWBERRY ┆ quirrel │
└────────┴────────────┴──────────┴──────────┴───────────┴───────────┘
一個比較有趣的範例是截取每一列除了第一個及最後一個字元之外的字串,可以透過搭配pl.Expr.str.slice()
及pl.Expr.str.len_chars()
來達成:
(
df.with_columns(
pl.all()
.str.slice(1, pl.all().str.len_chars() - 2)
.name.suffix("_s")
)
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits ┆ animals ┆ colors_s ┆ fruits_s ┆ animals_s │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ str ┆ str ┆ str │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black ┆ APPLE ┆ dog ┆ lac ┆ PPL ┆ o │
│ white ┆ BANANA ┆ cat ┆ hit ┆ ANAN ┆ a │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ ello ┆ TRAWBERR ┆ quirre │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘
pl.all().str.len_chars() - 2
中的-2
,代表減去起始及最終兩個字元。
codepanda
Pandas的string相關型別,也是相當令人困惑。大致上可以分為v0時代的object
型別、v1時代引入試圖支援pyarrow的string[pyarrow]
型別及v2時代引入的pd.ArrowDtype(pa.string())
型別。
df_pd = pd.DataFrame({"v0_object": ["black"]}).assign(
v1_stringarrow=lambda df_: df_.v0_object.astype(
{"v0_object": "string[pyarrow]"}
),
v2_stringpa=lambda df_: df_.v0_object.astype(
{"v0_object": pd.ArrowDtype(pa.string())}
),
)
print(df_pd.dtypes)
v0_object object
v1_stringarrow string[pyarrow]
v2_stringpa string[pyarrow]
dtype: object
如果只觀察dtypes會以為stringarrow
及stringpa
兩個是同一型別。
但如果將兩者進行is
或是==
比較,會發現都是False
:
print(
df_pd.dtypes["v1_stringarrow"] is df_pd.dtypes["v2_stringpa"],
df_pd.dtypes["v1_stringarrow"] == df_pd.dtypes["v2_stringpa"],
)
False False
雖然演變過程令人崩潰,但是pd.ArrowDtype(pa.string())
的確帶來顯著的效能提升,建議如果是以v2在做開發的朋友,可以大膽嘗試。
註1:Polars的正則表達式寫法與純Python略有不同,需要參考Rust的regex crate 說明文件。